package com.fp.m7.server.services;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.fp.m7.api.manager.AccountManager;
import com.fp.m7.api.manager.FollowManager;
import com.fp.m7.api.model.Account;
import com.fp.m7.server.auth.AuthenticationChecker;
import com.fp.m7.server.auth.M7PasswordEncoder;
import com.fp.m7.server.helpers.AccountDtoFactory;
import com.fp.m7.server.helpers.StickyCaptchaChecker;
import com.fp.m7.server.helpers.UserDtoFactory;
import com.fp.m7.shared.Constants;
import com.fp.m7.shared.ValidationConstants;
import com.fp.m7.shared.ValidationError;
import com.fp.m7.shared.ValidationResult;
import com.fp.m7.shared.dto.AccountDto;
import com.fp.m7.shared.dto.UserDto;
import com.fp.m7.shared.exception.GwtAuthenticationException;
import com.fp.m7.shared.services.AccountService;
import com.fp.m7.shared.validator.NewPasswordsValidator;
import com.fp.m7.shared.validator.RegistrationValidator;
import com.fp.m7.shared.validator.StatusAndDescriptionValidator;

/**
 * The server side implementation of the Accounts RPC service.
 */
@Service("accounts")
@Transactional
public class AccountServiceImpl implements AccountService {
    private static final Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);

    @Autowired
    private AuthenticationChecker authChecker;

    @Autowired
    private AccountManager accountManager;

    @Autowired
    private FollowManager followManager;

    @Autowired
    private StickyCaptchaChecker captchaChecker;

    @Autowired
    private M7PasswordEncoder passwordEncoder;

    @Autowired
    private AccountDtoFactory accountDtoFactory;

    @Autowired
    private UserDtoFactory userDtoFactory;

    @Autowired
    private RegistrationValidator registrationValidator;

    @Autowired
    private NewPasswordsValidator newPasswordsValidator;

    @Autowired
    private StatusAndDescriptionValidator statusAndDescriptionValidator;

    @Transactional(readOnly = true)
    @Override
    public Boolean checkUsernameAvailability(String username) {
        Account found = accountManager.findByUsername(username);
        return found == null;
    }

    @Transactional(readOnly = true)
    @Override
    public Boolean checkNicknameAvailability(String nickname) {
        Account found = accountManager.findByNickname(nickname);
        return found == null;
    }

    @Override
    public ValidationResult newAccount(String username, String password, String nickname, String captchaText) {
        ValidationResult result = registrationValidator.validate(username, nickname, password, password);

        if (!captchaChecker.validateStickyCaptcha(captchaText)) {
            result.add(new ValidationError(ValidationConstants.Fields.CAPTCHA, ValidationConstants.Messages.INCORRECT));
        }

        if (!result.isOk()) {
            return result;
        }

        // if all the regexp and captcha tests passed - go one step further: check database availability
        if (!checkUsernameAvailability(username)) {
            return new ValidationResult(new ValidationError(ValidationConstants.Fields.USERNAME,
                    ValidationConstants.Messages.NOT_AVAILABLE));
        }

        if (!checkNicknameAvailability(nickname)) {
            return new ValidationResult(new ValidationError(ValidationConstants.Fields.NICKNAME,
                    ValidationConstants.Messages.NOT_AVAILABLE));
        }

        String encodedPassword = passwordEncoder.encodePassword(username, password);

        accountManager.createNew(username, encodedPassword, nickname);
        return new ValidationResult();
    }

    @Secured({ "ROLE_USER" })
    @Override
    public Boolean deleteAccount() throws GwtAuthenticationException {
        Account found = authChecker.getLoggedAccount();
        accountManager.delete(found);
        return true;
    }

    @Secured({ "ROLE_USER" })
    @Override
    public Boolean changeNickname(String newNickname) throws GwtAuthenticationException {
        if (newNickname == null || !newNickname.matches(ValidationConstants.NICKNAME_REGEX)) {
            return false;
        }
        if (!checkNicknameAvailability(newNickname)) {
            return false;
        }
        Account user = authChecker.getLoggedAccount();
        user.setNickname(newNickname);
        accountManager.update(user);
        return true;
    }

    @Secured({ "ROLE_USER" })
    @Override
    public Boolean updateStatus(String newStatus) throws GwtAuthenticationException {
        ValidationResult result = statusAndDescriptionValidator.validateStatus(newStatus);
        if (!result.isOk()) {
            return false;
        }

        Account account = authChecker.getLoggedAccount();
        account.setStatus(newStatus);
        accountManager.update(account);
        return true;
    }

    @Secured({ "ROLE_USER" })
    @Override
    public Boolean updateDescription(String newDescription) throws GwtAuthenticationException {
        ValidationResult result = statusAndDescriptionValidator.validateDescription(newDescription);
        if (!result.isOk()) {
            return false;
        }

        Account account = authChecker.getLoggedAccount();
        account.setDescription(newDescription);
        accountManager.update(account);
        return true;
    }

    @Secured({ "ROLE_USER" })
    @Override
    public Boolean updateStatusAndDescription(String newStatus, String newDescription)
            throws GwtAuthenticationException {
        ValidationResult result = statusAndDescriptionValidator.validate(newStatus, newDescription);
        if (!result.isOk()) {
            return false;
        }

        Account account = authChecker.getLoggedAccount();
        account.setStatus(newStatus);
        account.setDescription(newDescription);
        accountManager.update(account);
        return true;
    }

    @Transactional(readOnly = true)
    @Override
    public AccountDto getDetails(String nickname) {
        Account user = accountManager.findByNickname(nickname);
        if (user != null) {
            return buildAccountDtoWithStats(user);
        }
        return null;
    }

    @Transactional(readOnly = true)
    @Override
    public AccountDto getAuthenticatedAccount() throws GwtAuthenticationException {
        AccountDto accountDto = null;
        try {
            // find the account itself:
            Account account = authChecker.getLoggedAccount();
            accountDto = buildAccountDtoWithStats(account);
        } catch (AuthenticationException e) {
            // this method is not secured, so authentication exception can be thrown
            logger.info("Unauthorized access attempt");
        }
        return accountDto;
    }

    @Override
    @Secured("ROLE_USER")
    public Boolean updateAvatarUrl(String newAvatarUrl) throws GwtAuthenticationException {
        Account authAccount = authChecker.getLoggedAccount();
        authAccount.setAvatarUrl(newAvatarUrl);
        accountManager.update(authAccount);
        return true;
    }

    @Override
    @Secured("ROLE_USER")
    public ValidationResult changePassword(String oldPassword, String newPassword) throws GwtAuthenticationException {
        Account account = authChecker.getLoggedAccount();

        ValidationResult result = newPasswordsValidator.validate(newPassword, newPassword);
        if (!result.isOk()) {
            return result;
        }

        String encodedOldPassword = passwordEncoder.encodePassword(account.getUsername(), oldPassword);
        if (!encodedOldPassword.equals(account.getPassword())) {
            return new ValidationResult(new ValidationError(ValidationConstants.Fields.PASSWORD,
                    ValidationConstants.Messages.INCORRECT));
        }

        String encodedNewPassword = passwordEncoder.encodePassword(account.getUsername(), newPassword);
        account.setPassword(encodedNewPassword);
        accountManager.update(account);

        return new ValidationResult();
    }

    @Transactional(readOnly = true)
    @Override
    public List<UserDto> getRandomUsers(int maxCount) {
        if (maxCount > Constants.MAX_RANDOM_USERS_TO_FIND) {
            maxCount = Constants.MAX_RANDOM_USERS_TO_FIND;
        }

        List<Account> randomlyPickedAccounts = accountManager.findRandom(maxCount);
        return userDtoFactory.build(randomlyPickedAccounts);
    }

    protected AccountDto buildAccountDtoWithStats(Account account) {
        assert (account != null);

        AccountDto accountDto = accountDtoFactory.build(account);

        // put additional stats:
        int followersCount = followManager.countActiveFollowers(account);
        int followingCount = followManager.countActivelyFollowed(account);
        accountDto.setFollowersCount(followersCount);
        accountDto.setFollowingCount(followingCount);

        return accountDto;
    }

    public void setAuthChecker(AuthenticationChecker authChecker) {
        this.authChecker = authChecker;
    }
    
    public void setAccountManager(AccountManager accountManager) {
		this.accountManager = accountManager;
	}
    
    public void setFollowManager(FollowManager followManager) {
		this.followManager = followManager;
	}

    public void setCaptchaChecker(StickyCaptchaChecker captchaChecker) {
        this.captchaChecker = captchaChecker;
    }

    public void setPasswordEncoder(M7PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    public void setAccountDtoFactory(AccountDtoFactory accountDtoFactory) {
        this.accountDtoFactory = accountDtoFactory;
    }

    public void setUserDtoFactory(UserDtoFactory userDtoFactory) {
        this.userDtoFactory = userDtoFactory;
    }

    public void setRegistrationValidator(RegistrationValidator registrationValidator) {
        this.registrationValidator = registrationValidator;
    }

    public void setNewPasswordsValidator(NewPasswordsValidator newPasswordsValidator) {
        this.newPasswordsValidator = newPasswordsValidator;
    }

    public void setStatusAndDescriptionValidator(StatusAndDescriptionValidator statusAndDescriptionValidator) {
        this.statusAndDescriptionValidator = statusAndDescriptionValidator;
    }
}
